import { $set } from '@feng3d/serialization';
import { BoxGeometry3D, BufferAttribute3D, BufferGeometry3D, CatmullRomCurve3, Color, Line3D, LineBasicMaterial3D, Mesh3D, MeshLambertMaterial3D, Node3D, PerspectiveCamera3D, PlaneGeometry3D, Raycaster3D, Scene3D, ShadowMaterial3D, Vector2, Vector3, WebGLRenderer3D } from 'feng3d-three';
import GUI from 'lil-gui';
import '../../extension/controls/OrbitControls3D';
import '../../extension/controls/TransformControls3D';
import { TransformControls3D } from '../../extension/controls/TransformControls3D';

let container: HTMLElement;
let camera: PerspectiveCamera3D;
let scene: Scene3D;
let renderer: WebGLRenderer3D;
const splineHelperObjects: Mesh3D[] = [];
let splinePointsLength = 4;
const positions: Vector3[] = [];
const point = new Vector3();

const raycaster = new Raycaster3D();
const pointer = new Vector2();
const onUpPosition = new Vector2();
const onDownPosition = new Vector2();

const geometry = $set(new BoxGeometry3D(), { width: 20, height: 20, depth: 20 });
let transformControl: TransformControls3D;

const ARC_SEGMENTS = 200;

const splines: {
    [key: string]: { mesh: Line3D } & CatmullRomCurve3,
} = {};

const params = {
    uniform: true,
    tension: 0.5,
    centripetal: true,
    chordal: true,
    addPoint,
    removePoint,
    exportSpline
};

init();

function init()
{
    container = document.getElementById('container');

    scene = new Node3D().addComponent('Scene3D');
    scene.background = new Color(0xf0f0f0);

    camera = new Node3D().addComponent('PerspectiveCamera3D', { fov: 70, aspect: window.innerWidth / window.innerHeight, near: 1, far: 10000 });
    camera.entity.position.set(0, 250, 1000);
    scene.entity.addChild(camera.entity);

    scene.addComponent('AmbientLight3D', { color: { value: 0xf0f0f0 } });
    const light = new Node3D().addComponent('SpotLight3D', { color: { value: 0xffffff }, intensity: 1.5 });
    light.entity.position.set(0, 1500, 200);
    light.angle = Math.PI * 0.2;
    light.castShadow = true;
    light.shadow.camera.near = 200;
    light.shadow.camera.far = 2000;
    light.shadow.bias = -0.000222;
    light.shadow.mapSize.width = 1024;
    light.shadow.mapSize.height = 1024;
    scene.entity.addChild(light.entity);

    const planeGeometry = $set(new PlaneGeometry3D(), { width: 2000, height: 2000 });
    planeGeometry.rotateX(-Math.PI / 2);
    const planeMaterial = new ShadowMaterial3D({ color: 0x000000, opacity: 0.2 });

    const plane = new Node3D().addComponent('Mesh3D', { geometry: planeGeometry, material: planeMaterial });
    plane.entity.position.y = -200;
    plane.receiveShadow = true;
    scene.entity.addChild(plane.entity);

    const helper = new Node3D().addComponent('GridHelper3D', { size: 2000, divisions: 100 });
    helper.entity.position.y = -199;
    helper.material.opacity = 0.25;
    helper.material.transparent = true;
    scene.entity.addChild(helper.entity);

    renderer = scene.addComponent('WebGLRenderer3D', { parameters: { antialias: true } });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    container.appendChild(renderer.domElement);

    const gui = new GUI();

    gui.add(params, 'uniform').onChange(render);
    gui.add(params, 'tension', 0, 1).step(0.01).onChange(function (value)
    {
        splines.uniform['tension'] = value;
        updateSplineOutline();
        render();
    });
    gui.add(params, 'centripetal').onChange(render);
    gui.add(params, 'chordal').onChange(render);
    gui.add(params, 'addPoint');
    gui.add(params, 'removePoint');
    gui.add(params, 'exportSpline');
    gui.open();

    // Controls
    const controls = new Node3D().addComponent('OrbitControls3D', { camera, domElement: renderer.domElement });
    controls.damping = 0.2;
    controls.emitter.on('change', render);

    transformControl = new Node3D().addComponent('TransformControls3D', { camera, domElement: renderer.domElement });
    transformControl.emitter.on('change', render);
    transformControl.emitter.on('dragging-changed', function (event)
    {
        controls.enabled = !event.data.value;
    });
    scene.entity.addChild(transformControl.entity);

    transformControl.emitter.on('objectChange', function ()
    {
        updateSplineOutline();
    });

    document.addEventListener('pointerdown', onPointerDown);
    document.addEventListener('pointerup', onPointerUp);
    document.addEventListener('pointermove', onPointerMove);
    window.addEventListener('resize', onWindowResize);

    /** *****
     * Curves
     *********/

    for (let i = 0; i < splinePointsLength; i++)
    {
        addSplineObject(positions[i]);
    }

    positions.length = 0;

    for (let i = 0; i < splinePointsLength; i++)
    {
        positions.push(splineHelperObjects[i].entity.position);
    }

    const geometry = new BufferGeometry3D();
    geometry.setAttribute('position', new BufferAttribute3D(new Float32Array(ARC_SEGMENTS * 3), 3));

    let curve: { mesh: Line3D } & CatmullRomCurve3 = new CatmullRomCurve3(positions) as any;
    curve.curveType = 'catmullrom';
    curve.mesh = new Node3D().addComponent('Line3D', {
        geometry: geometry.clone(), material: new LineBasicMaterial3D({
            color: 0xff0000,
            opacity: 0.35
        })
    });
    curve.mesh.castShadow = true;
    splines.uniform = curve as any;

    curve = new CatmullRomCurve3(positions) as any;
    curve.curveType = 'centripetal';
    curve.mesh = new Node3D().addComponent('Line3D', {
        geometry: geometry.clone(), material: new LineBasicMaterial3D({
            color: 0x00ff00,
            opacity: 0.35
        })
    });
    curve.mesh.castShadow = true;
    splines.centripetal = curve;

    curve = new CatmullRomCurve3(positions) as any;
    curve.curveType = 'chordal';
    curve['mesh'] = new Node3D().addComponent('Line3D', {
        geometry: geometry.clone(), material: new LineBasicMaterial3D({
            color: 0x0000ff,
            opacity: 0.35
        })
    });
    curve.mesh.castShadow = true;
    splines.chordal = curve;

    for (const k in splines)
    {
        const spline = splines[k];
        scene.entity.addChild(spline.mesh.entity);
    }

    load([new Vector3(289.76843686945404, 452.51481137238443, 56.10018915737797),
    new Vector3(-53.56300074753207, 171.49711742836848, -14.495472686253045),
    new Vector3(-91.40118730204415, 176.4306956436485, -6.958271935582161),
    new Vector3(-383.785318791128, 491.1365363371675, 47.869296953772746)]);

    render();
}

function addSplineObject(position?: Vector3)
{
    const material = new MeshLambertMaterial3D({ color: Math.random() * 0xffffff });
    const object = new Node3D().addComponent('Mesh3D', { geometry, material });

    if (position)
    {
        object.entity.position.copy(position);
    }
    else
    {
        object.entity.position.x = Math.random() * 1000 - 500;
        object.entity.position.y = Math.random() * 600;
        object.entity.position.z = Math.random() * 800 - 400;
    }

    object.castShadow = true;
    object.receiveShadow = true;
    scene.entity.addChild(object.entity);
    splineHelperObjects.push(object);

    return object;
}

function addPoint()
{
    splinePointsLength++;

    positions.push(addSplineObject().entity.position);

    updateSplineOutline();

    render();
}

function removePoint()
{
    if (splinePointsLength <= 4)
    {
        return;
    }

    const point = splineHelperObjects.pop();
    splinePointsLength--;
    positions.pop();

    if (transformControl.object === point._mesh) transformControl.detach();
    scene.entity.removeChild(point.entity);

    updateSplineOutline();

    render();
}

function updateSplineOutline()
{
    for (const k in splines)
    {
        const spline = splines[k];

        const splineMesh = spline.mesh;
        const position = splineMesh.geometry.attributes.position as BufferAttribute3D;

        for (let i = 0; i < ARC_SEGMENTS; i++)
        {
            const t = i / (ARC_SEGMENTS - 1);
            spline.getPoint(t, point);
            position.setXYZ(i, point.x, point.y, point.z);
        }

        position.needsUpdate = true;
    }
}

function exportSpline()
{
    const strplace: string[] = [];

    for (let i = 0; i < splinePointsLength; i++)
    {
        const p = splineHelperObjects[i].entity.position;
        strplace.push(`new THREE.Vector3(${p.x}, ${p.y}, ${p.z})`);
    }

    console.log(strplace.join(',\n'));
    const code = `[${strplace.join(',\n\t')}]`;
    prompt('copy and paste code', code);
}

function load(newPositions: Vector3[])
{
    while (newPositions.length > positions.length)
    {
        addPoint();
    }

    while (newPositions.length < positions.length)
    {
        removePoint();
    }

    for (let i = 0; i < positions.length; i++)
    {
        positions[i].copy(newPositions[i]);
    }

    updateSplineOutline();
}

function render()
{
    splines.uniform.mesh.entity.visible = params.uniform;
    splines.centripetal.mesh.entity.visible = params.centripetal;
    splines.chordal.mesh.entity.visible = params.chordal;
    renderer.render(scene, camera);
}

function onPointerDown(event: PointerEvent)
{
    onDownPosition.x = event.clientX;
    onDownPosition.y = event.clientY;
}

function onPointerUp(event: PointerEvent)
{
    onUpPosition.x = event.clientX;
    onUpPosition.y = event.clientY;

    if (onDownPosition.distanceTo(onUpPosition) === 0) transformControl.detach();
}

function onPointerMove(event: PointerEvent)
{
    pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
    pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(pointer, camera);

    const intersects = raycaster.intersectObjects(splineHelperObjects.map((v) => v.entity), false);

    if (intersects.length > 0)
    {
        const object = intersects[0].object;

        if (object.parent !== transformControl.object)
        {
            transformControl.attach(object.parent);
        }
    }
}

function onWindowResize()
{
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);

    render();
}
